적응자 패턴

adapter란?

  • adapter는 다른 전기나 기계 장치를 서로 연결해서 작동할 수 있도록 만들어 주는 결합 도구를 뜻합니다.

디자인 패턴 분류

  • 디자인 패턴에는 생성, 구조, 행위, 3가지 분류가 있습니다.
  • 어댑터 패턴은 구조에 대한 패턴입니다.

구조 패턴

  • 구조 패턴이란 작은 클래스들을 상속합성을 이용하여 더 큰 클래스를 생성하는 방법을 제공하는 패턴입니다.
  • 서로 독립적으로 개발한 클래스 라이브러리를 마치 하나인 것처럼 사용할 수 있습니다. 또, 여러 인터페이스를 합성(Composite)하여 서로 다른 인터페이스들의 통일된 추상을 제공합니다.
  • 구조 패턴의 중요한 포인트는 인터페이스나 구현을 복합하는 것이 아니라 객체를 합성하는 방법을 제공한다는 것입니다. 이는 컴파일 단계에서가 아닌 런타임 단계에서 복합 방법이나 대상을 변경할 수 있다는 점에서 유연성을 갖습니다.

호환성을 위한 패턴

  • 일상 생활에서와 동일하게 어떤 인터페이스를 클라이언트에서 요구하는 형태의 인터페이스에 적응시켜주는 역할을 한다.
  • 어뎁터 패턴은 B를 A처럼 포장하여 A로 사용할 수 있게 하는 패턴입니다.
  • 이미 잘 구축되어 있는 것을 새로운 어떤 것이 사용 할 때 양쪽 간의 호환성을 유지해주기 위해 사용합니다.
  • 한 클래스의 인터페이스를 클라이언트에서 사용하고자하는 다른 인터페이스로 변환합니다.
  • 어댑터를 구현할 때는 타켓 인터페이스의 크기와 구종에 따라 코딩해야 할 분량이 결정됩니다.

활용 상황

  • 이미 만든 것을 재사용하고자 하나 이 재사용 가능한 라이브러리를 수정할 수 없을 때 사용합니다.
    • 서버 코드에 손대지 못한다면 어댑터 패턴을 사용해 API를 변경한 후 잠금을 추가 합니다.
  • 기존의 코드에 새로운 코드(써드파티 라이브러리 등)을 연동하여 사용하고 싶은데, 두 코드의 인터페이스가 달라, 이를 하나로 통일하여 사용하고 싶을 때 사용합니다.
  • 리팩토링 없이도 기존의 클래스를 이용해 새로운 클래스를 만들 수 있습니다.
  • [객체 적응자] 이미 존재하는 여러 개의 서브 클래스를 사용해야 하는데, 이 서브클래스들의 상속을 통해서 이들의 인터페이스를 다 개조 한다는 것이 현실성이 없을 때, 객체 적응자를 써서 부모 클래스의 인터페이스를 변경하는 것이 더 바람직합니다.

장점

  • 관계가 없는 인터페이스를 같이 사용할 수 있습니다.
  • 기존 클라이언트 단의 코드 수정을 최소화 할 수 있습니다.
  • 클래스 재활용성이 증가합니다.
  • 클라이언트는 연동부분을 몰라도, 새로운 코드의 기능을 일관되게 사용 가능합니다.

장점을 조금 더 길게 설명해 보겠습니다.

  • 기존 클래스의 소스코드를 수정해서 인터페이스에 맞추는 작업보다는 기존 클래스의 소스 코드의 수정을 전혀 하지 않고 타겟 인터페이스에 맞춰서 동작을 가능하게 합니다. 즉, 기존 클래스의 명세만 알면 얼마든지 새로운 클래스를 작성할 수 있습니다. 이를 통해 소스코드가 간단해지고 유지볻보수도 원할하게 하는 이점이 있습니다.
  • 어댑터를 이용하면 인터페이스 호환성 문제 때문에 같이 쓸 수 없는 클래스들을 연결해서 쓸 수 있습니다. 이로 인해 호환되지 않는 인터페이스를 사용하는 클라이언트를 그대로 활용할 수 있습니다.
  • 어댑터 패턴을 통해 클라이언트와 구현된 인터페이스를 분리시킬수 있으며, 향후 인터페이스가 바뀌더라도 그 변경 내역은 어댑터에 캡슐화 되기 때문에 클라이언트는 바뀔 필요가 없어집니다.
  • 외부 패키지를 호출하는 코드를 가능한 줄여 경계를 관리합니다. 코드 가독성이 높아지며, 경계 인터페이스를 사용하는 일관성도 높아지며, 외부 패키지가 변했을 때 변경할 코드도 줄어듭니다.

단점

  • 어댑터 클래스에서 통일 시켜주는 부분을 구현해야 합니다.

객체지향 원칙

  • 어댑터 패턴에는 여러 객체지향 원칙이 반영되어 있습니다. 어댑터를 새로 바뀐 인터페이스로 감쌀 때는 객체 구성(composition)을 사용합니다. 이런 접근법을 쓰면 어탭티의 어떤 서브클래스에 대해서도 어댑터를 쓸 수 있다는 장점이 있지요.

클라이언트에서 어댑터를 사용하는 방법에 대해 살펴 보겠습니다.

  1. 클라이언트에서 타겟 인터페이스를 사용하여 메소드를 호출함으로써 어댑터에 요청합니다.
  2. 어댑터에서는 어댑티 인터페이스를 사용하여 그 요청을 어댑티에 대한 하나 이상의 메소드를 호출로 변환합니다.
  3. 클라이언트에서는 호출 결과를 받긴 하지만 중간에 어댑터가 껴 있는지는 전혀 알지 못합니다.
  • 클라이언트 -> request() -> 어댑터 - specificRequest() -> 어댑티.
  • 클라이언트에서는 Target Interface를 호출하는 것처럼 보입니다. 하지만 클라이언트의 요청을 전달받은 (Target Interface 를 구현한) Adapter 는 자신이 감싸고 있는 Adaptee에게 실질적인 처리를 위임합니다. Adapter가 Adaptee를 감싸고 있는 것 때문에 Wrapper 패턴이라고도 불립니다.

어댑터에는 두종류가 있다.

클래스 어댑터 패턴

  • 상속을 이용한 어댑터 패턴입니다.
  • 클래스 어댑터에서는 어댑터를 만들 때 타겟과 어댑티 모두의 서브클래스로 만들고, 객체 어댑터에서는 구성을 통해서 어댑티에 요청을 전달한다는 점을 제외하면 별 다르 차이점이 없습니다.
  • 특정 어댑티 클래스에만 적용된다는 단점이 있습니다. 대신 어댑티 전체를 다시 구현하지 않아도 된다는 장점이 있습니다. 그리고 서브클래스기 때문에 어댑티의 행동을 오버라이드할 수 있습니다.
  • 클래스 어댑터 패턴은 다중 상속을 허용하는 프로그래밍 언어에서만 가능한 패턴이다.

객체 어댑터 패턴

  • 구성(composition)을 사용하기 때문에 더 뛰어납니다. 어댑티 클래스 뿐 아니라 그 서브 클래스에 대해서도 어댑터 역할을 할 수 있습니다.
  • 상속이 아닌 구성을 활용합니다. 상속을 이용하면 코드 분량을 줄일 수 있긴 하겠지만, 구성을 이용하더라도 어댑티한테 필요한 일을 시키기 위한 코드만 만들면 되기 때문에 별로 코드가 많이 필요한 건 아닙니다. 유연성을 확보할 수 있습니다.

클래스 어댑터 vs 객체 어댑터

  • 클래스 어댑터는 다중 상속이 불가능합니다.
  • 클래스의 상속 기능을 사용할 경우 조합 폭발이 일어나서 제어 불가능합니다.
    • 상속의 남용으로 하나의 기능을 추가하기 위해 필요 이상으로 많은 수의 클래스를 추가해야 하는 경우를 가르켜 클래스 폭발(class explosion) 문제 또는 조합 폭발(combinational explosion) 문제라고 부릅니다.
    • 런타임에 타입선택(세트)
    • 추상메소드로 의존성 역전
  • 객체 어댑터의 경우 의존성 폭발이 일어나지만 제어 가능합니다.
    • 런타임에 합성(조립)
    • 추가 인터페이스로 의존성 분산

유사 패턴과의 비교

  • 어댑터 : 한 인터페이스를 다른 인터페이스로 변환
  • 데코레이터 : 인터페이스는 바꾸지 않고 책임(기능)만 추가
  • 퍼사드 : 인터페이스를 간단하게 바꿈

퍼사드 vs 어댑터

  • 퍼사드는 인터페이스를 단순화 시킬 뿐 아니라 클라이언트와 구성요소들로 이루어진 서브시스템을 분리시키는 역할도 합니다.
  • 퍼사드와 어댑터는 모두 여러 개의 클래스를 감쌀 수 있습니다. 하지만 퍼사드는 인터페이스를 단순화시키기 위한 용도로 쓰이는 반면, 어댑터는 인터페이스를 다른 인터페이스로 변환하기 위한 용도로 쓰입니다.
  • 퍼사드는 한 객체에 다른 인터페이스를 구현한다는 면에서 어댑처와 유사합니다. 그러나 퍼사드는 새로운 인터페이스를 생성하지만 어댑터는 기존 인터페이스를 재활용한다는 점이 다릅니다.

어댑터와 프록시의 차이

  • 공통점 : 클라이언트와 다른 객체 사이에 끼어들어서 클라이언트로부터 요청을 받아와서 다른 객체한테 전달해주는 역할을 합니다.
  • 어댑터 패턴 : 다른 객체의 인터페이스를 바꿔줍니다.
  • 프록시 패턴 : 똑같은 인터페이스를 사용합니다.
  • 보호 프록시 : 보호 프록시에서는 클라이언트의 역할에 따라서 객체에 있는 특정 메소드에 대한 클라이언트 접근을 제어합니다. 그러다 보니 보호 프록시에서는 클라이언트한테 인터페이스의 일부분만을 제공할 수 있습니다. 이런 점은 어댑터하고 비슷하다고 할 수 있습니다.

구성

Target Interface

Adapter 가 구현(implements) 하는 인터페이스이다. 클라이언트는 Target Interface 를 통해 Adaptee 인 써드파티 라이브러리를 사용하게 된다.

interface MediaPlayer {
  play: (fileName: string) => void;
}

class MP3 implements MediaPlayer {
  play(fileName) {
    console.log(`MP3 : ${fileName}`);
  }
}

Adaptee

  • 써드파티 라이브러리나 외부시스템을 의미한다.
interface MediaPackage {
  playFile: (fileName: string) => void;
}

class MP4 implements MediaPackage {
  playFile(fileName) {
    console.log(`MP4 : ${fileName}`);
  }
}

class MKV implements MediaPackage {
  playFile(fileName) {
    console.log(`MKV : ${fileName}`);
  }
}

Adapter

  • Client 와 Adaptee 중간에서 호환성이 없는 둘을 연결시켜주는 역할을 담당한다. Target Interface 를 구현하며, 클라이언트는 Target Interface 를 통해 어댑터에 요청을 보낸다. 어댑터는 클라이언트의 요청을 Adaptee 가 이해할 수 있는 방법으로 전달하고, 처리는 Adaptee 에서 이루어진다.
// 객체 어댑터
// MP3를 상속하여 사용하는 것도 가능
class FormatAdapter implements MediaPlayer {
  media: MediaPackage;

  constructor(media: MediaPackage) {
    this.media = media;
  }

  play(fileName) {
    console.log('Using Adapter');
    this.media.playFile(fileName);
  }
}

// 클래스 어댑터
// MP3를 상속하여 사용하는 것은 불가능
class FormatAdapterMP4 extends MP4 implements MediaPlayer {
  play(fileName) {
    console.log('Using Adapter');
    this.playFile(fileName);
  }
}
class FormatAdapterMKV extends MKV implements MediaPlayer {
  play(fileName) {
    console.log('Using Adapter');
    this.playFile(fileName);
  }
}

Client

  • 써드파티 라이브러리나 외부시스템을 사용하려는 쪽이다.
// 객체 어댑터
const playerMP3 = new MP3();
playerMP3.play('file.mp3');
const playerMP4 = new FormatAdapter(new MP4());
playerMP4.play('file.mp4');
const playerMKV = new FormatAdapter(new MKV());
playerMKV.play('file.mkv');

// 클래스 어댑터
const playerMP3 = new MP3();
playerMP3.play('file.mp3');
const playerMP4 = new FormatAdapterMP4();
playerMP4.play('file.mp4');
const playerMKV = new FormatAdapterMKV();
playerMP4.play('file.mkv');

어댑터 패턴 정리

  • Adaptee를 감싸고, Target Interface만을 클라이언트에게 드러냅니다.
  • Target Interface를 구현하여 클라이언트가 예상하는 인터페이스가 되도록 Adaptee의 인터페이스를 간접적으로 변경합니다.
  • Adaptee가 기대하는 방식으로 클라이언트의 요청을 간접적으로 변경합니다.
  • 호환되지 않는 우리의 인터페이스와 Adaptee를 함께 사용할 수 있습니다.

참고